﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq.Expressions;

namespace Kocic.RepositoryManagement.SystemFile
{
    /// <summary>
    /// Defines methods and operations for managers to perform create, read, update, and delete operations on a line object, in a file.
    /// </summary>
    /// <typeparam name="TLineObject">The type of the line object that the entity data manager operations will be implemented for.</typeparam>
    /// <typeparam name="TContext">The type of the file context that will be used by the entity data managers.</typeparam>
    public abstract class SystemFileRepositoryManager<TLineObject, TContext> : DataManager<TLineObject, TContext>, IRetrievable<TLineObject>
        where TLineObject : LineObject
        where TContext : FileContext, new()
    {

        /// <summary>
        /// Creates an instance of the FileManager class.
        /// </summary>
        /// <param name="context">An instance of the file context that will be used for operations.</param>
        /// <exception cref="ArgumentNullException"></exception>
        protected SystemFileRepositoryManager(TContext context)
            : base(context)
        {

        }

        /// <summary>
        /// Creates an instance of the SystemFileRepositoryManager class. Invoking this constructor will pass the static instance reference to the object context referenced an instance of the SystemFileExecutionContext class.
        /// </summary>
        protected SystemFileRepositoryManager()
            : this(SystemFileExecutionContext<TContext>.CurrentDataSource)
        {

        }

        /// <summary>
        /// Disposes unmanaged resources.
        /// </summary>
        /// <param name="disposing">Value indicating if to invoke the method from the IDisposable.Dispose implementation or from the finalizer.</param>
        protected override void Dispose(Boolean disposing)
        {
            if (disposing)
            {
                if (this.IsDisposed)
                {
                    return;
                }
                /*Checks to
                 * see if the context was set in this fashion. If yes, then we do not dispose of the context yet. */
                if (SystemFileExecutionContext<TContext>.CurrentDataSource != null)
                {
                    this.DisposeDataSource = false;
                }

                if (this.DisposeDataSource)
                {

                    this.DataSource.Dispose();
                }

                base.Dispose(disposing);
            }
        }

        /// <summary>
        /// Saves changes to the file context and commits them to the file.
        /// </summary>
        /// <param name="lineObject">The line object to commit to the file.</param>
        protected override void Commit(TLineObject lineObject = null)
        {
            this.DataSource.SaveChanges();
        }

        /// <summary>
        /// Performs operations to add the line object to the file context.
        /// </summary>
        /// <param name="lineObject">The line object to add.</param>
        protected override void DataSourceCreateOperations(TLineObject lineObject)
        {
            this.DataSource.AddObject(lineObject);
        }

        /// <summary>
        /// Performs operations to update the line object in the file context.
        /// </summary>
        /// <param name="lineObject">The line object to update.</param>
        protected override void DataSourceUpdateOperations(TLineObject lineObject)
        {
            this.DataSource.UpdateObject(lineObject);
        }

        /// <summary>
        /// Performs operations to remove the line object from the file context.
        /// </summary>
        /// <param name="lineObject">The entity object to remove.</param>
        protected override void DataSourceDeleteOperations(TLineObject lineObject)
        {
            this.DataSource.DeleteObject(lineObject);
        }

        /// <summary>
        /// Performs any additional operations with the file object before it is created and added to the context.
        /// </summary>
        /// <param name="lineObject">The file object used in performing additional operations to prior to adding and saving the changes to the context.</param>
        protected override TLineObject PreCreate(TLineObject lineObject)
        {
            return null;
        }

        /// <summary>
        /// Performs any additional operations with the line object before it is updated and its changes saved to the context.
        /// </summary>
        /// <param name="lineObject">The line object used in performing additional operations to prior to saving its changes to the context.</param>
        protected override TLineObject PreUpdate(TLineObject lineObject)
        {
            return null;
        }

        /// <summary>
        /// Performs any additional operations with the line object before it is deleted and removed from the context.
        /// </summary>
        /// <param name="lineObject">The line object used in performing additional operations to prior to deleting it from the context and saving the changes.</param>
        protected override TLineObject PreDelete(TLineObject lineObject)
        {
            return null;
        }

        #region IRetrievable<LineObject> Members

        /*These methods have been marked as abstract since the manager must be required to provide the basic read functionality */

        /// <summary>
        /// Defines how to return a single instance of the line object based on the specified search value.
        /// </summary>
        /// <param name="searchValue">An object used to store particular search criteria.</param>
        /// <returns>The line object that is a result of a search.</returns>
        public abstract TLineObject RetrieveSingle(Object searchValue);

        /// <summary>
        /// Defines how to retrieve multiple instances of the line object.
        /// </summary>
        /// <returns>An enumerator of a collection of type TLineObject.</returns>
        public abstract IEnumerable<TLineObject> RetrieveMultiple();

        /// <summary>
        /// Defines how to retrieve multiple instances of the line object based on the specified search value.
        /// </summary>
        /// <param name="searchValue">An object used to store particular search criteria.</param>
        /// <returns>An enumerator of a collection of type TEntityObject that is the result of a search.</returns>
        public abstract IEnumerable<TLineObject> RetrieveMultiple(Object searchValue);

        #endregion

        /// <summary>
        /// Check to see if the state of an line object is in the detached state.
        /// </summary>
        /// <param name="lineObject">The line object to check the state.</param>
        /// <returns>Value indiciating if the line object is in a detached state.</returns>
        protected override Boolean? IsDataObjectInRemovedState(TLineObject lineObject)
        {
            return lineObject.LineNumber.Equals(0);
        }

        /// <summary>
        /// Checks to see if a EntityContextDeferrer instance was instantiated and a reference exists.
        /// </summary>
        /// <returns>Value indicating if the EntityContextDeferrer was instantiated and a reference exists.</returns>
        protected override bool IsDeferralInstanceInstantiated()
        {
            return SystemFileContextDeferrer<TContext>.DeferralOperation != null;
        }

        /// <summary>
        /// Executes any deferral operations for the provided entity object.
        /// </summary>
        /// <param name="entityObject">The entity object to defer operations on.</param>
        protected override void DeferrerOperations(TLineObject entityObject)
        {
            if (this.IsDeferralInstanceInstantiated())
            {
                SystemFileContextDeferrer<TContext>.DeferredObjects.Add(entityObject);
            }
        }

        /// <summary>
        /// Saves a entity object to the context and the database. Either creates or updates the line object based on the object's line number state. If an instance of the
        /// EntityContextDeferral instance exists, then the entity object is simply added to the context and no changes are saved.
        /// </summary>
        /// <param name="lineObject">The line object to save to the file context.</param>
        /// <param name="context">A instance of the object context to which the line object will be saved to. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the EntityExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        public static void SaveLineObject(TLineObject lineObject, TContext context = null)
        {
            SystemFileRepositoryManager<TLineObject, TContext>.ContextOperations(lineObject, context, (inLineObject, inContext) =>
            {
                if (inLineObject.LineNumber == 0)
                {
                    inContext.AddObject(inLineObject);
                }
                else
                {
                    inContext.UpdateObject(inLineObject);
                }
            });
        }

        /// <summary>
        /// Saves a entity object to the context and the database. Either creates or updates the line object based on the object's line number state. If an instance of the
        /// EntityContextDeferral instance exists, then the entity object is simply added to the context and no changes are saved.
        /// </summary>
        /// <param name="lineObject">The line object to save to the file context.</param>
        /// <param name="lineNumber">The line number to which to save the line object.</param>
        /// <param name="context">A instance of the object context to which the line object will be saved to. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the EntityExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        public static void SaveLineObject(TLineObject lineObject, Int32 lineNumber, TContext context = null)
        {
            SystemFileRepositoryManager<TLineObject, TContext>.ContextOperations(lineObject, context, (inLineObject, inContext) =>
            {
                if (inLineObject.LineNumber == 0)
                {
                    inContext.AddObject(inLineObject, lineNumber);
                }
                else
                {
                    inContext.UpdateObject(inLineObject, lineNumber);
                }
            });
        }

        /// <summary>
        /// Retrieves and returns IQueryable&lt;LineObject> object based on the provided where expression.
        /// </summary>
        /// <param name="whereExpression">Where expression.</param>
        /// <param name="context">A instance of the object context to which all the line objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the SystemFileExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <returns>IQueryable&lt;LineObject> object.</returns>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static IQueryable<LineObject> Retrieve(Expression<Func<LineObject, Boolean>> whereExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (SystemFileExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = SystemFileExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && SystemFileExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the SystemFileExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            return currentContext.Lines.Where(whereExpression);
        }

        /// <summary>
        /// Retrieves and returns IQueryable&lt;LineObject> object based on the provided select expression.
        /// </summary>
        /// <param name="retrieveExpression">Retrieve expression.</param>
        /// <param name="context">A instance of the object context to which all the line objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the SystemFileExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <returns>IQueryable&lt;LineObject> object.</returns>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static IQueryable<LineObject> Retrieve(Expression<Func<LineObject, LineObject>> retrieveExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (SystemFileExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = SystemFileExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && SystemFileExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the SystemFileExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            return currentContext.Lines.Select(retrieveExpression);
        }

        /// <summary>
        /// Removes a data object from the object context and the database. If an instance of the
        /// SystemFileContextDeferral instance exists, then the entity object is simply removed and no changes are saved.
        /// </summary>
        /// <param name="lineObject">The entity object to delete and remove from the object context.</param>
        /// <param name="context">A instance of the object context to which the entity object will be removed from. If this instance is null, then a object context instance referenced
        /// by a SystemFileExecutionContext instance will be used. If this instance is not null and an instance of the SystemFileExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        public static void DeleteLineObject(TLineObject lineObject, TContext context = null)
        {
            SystemFileRepositoryManager<TLineObject, TContext>.ContextOperations(lineObject, context, (inLineObject, inContext) =>
            {
                inContext.DeleteObject(inLineObject);
            });
        }

        /// <summary>
        /// Deletes line objects based on the provided where expression.
        /// </summary>
        /// <param name="whereExpression">Where expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a SystemFileExecutionContext instance will be used. If this instance is not null and an instance of the SystemFileExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static void DeleteLineObjects(Expression<Func<LineObject, Boolean>> whereExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (SystemFileExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = SystemFileExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && SystemFileExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the SystemFileExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            var objects = currentContext.Lines.Where(whereExpression).ToList();

            foreach (var o in objects)
            {
                SystemFileRepositoryManager<LineObject, TContext>.DeleteLineObject(o);
            }
        }

        /// <summary>
        /// Deletes line objects based on the provided select expression.
        /// </summary>
        /// <param name="retrieveExpression">Retrieve expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the SystemFileExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static void DeleteLineObjects(Expression<Func<LineObject, LineObject>> retrieveExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (SystemFileExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = SystemFileExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && SystemFileExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the SystemFileExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            var objects = currentContext.Lines.Select(retrieveExpression).ToList();

            foreach (var o in objects)
            {
                SystemFileRepositoryManager<LineObject, TContext>.DeleteLineObject(o);
            }
        }

        /// <summary>
        /// Helper method which has the same logic when trying to add/remove entity objects in/from a context. Because the same logic is used, a delegate to the context's
        /// AddObject/DeleteObject methods is used.
        /// </summary>
        /// <param name="entityObject">The entity object for which the appropriate action will take place.</param>
        /// <param name="context">The object context for which the action upon the entity object will take place.</param>
        /// <param name="contextAction">A delegate to a method that will execute the appropriate context operations.</param>
        private static void ContextOperations(TLineObject entityObject, TContext context, Action<TLineObject, TContext> contextAction)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (SystemFileExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = SystemFileExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && SystemFileExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the SystemFileExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not bee instantiated.");
            }

            bool toDefer = SystemFileContextDeferrer<TContext>.DeferralOperation != null ? true : false;

            try
            {
                contextAction(entityObject, currentContext);

                if (!toDefer)
                {
                    currentContext.SaveChanges();
                }
                else
                {
                    SystemFileContextDeferrer<TContext>.DeferredObjects.Add(entityObject);
                }
            }
            catch
            {
                throw;
            }
        }
    }
}
